import math
import random
from collections import Counter


# 函数功能：从指定路径加载Iris数据集，对数据进行预处理，去除空行并将每行数据转换为特征-标签对的形式，最终返回处理后的数据集
# 参数说明：
# - file_path (str, 可选)：数据集文件的路径，默认为 'iris.data'，可以根据实际情况指定不同的路径
# 返回值说明：
# - 如果数据加载和预处理成功，返回一个包含特征-标签对的列表，每个元素为一个元组，元组的第一个元素是特征列表，第二个元素是对应的标签字符串；
# - 如果加载过程中出现文件未找到或数据格式错误等问题，返回空列表，并打印相应的错误信息
def load_iris_data(file_path='iris.data'):
    data = []
    try:
        with open(file_path, 'r') as f:
            lines = f.readlines()  # 一次性读取文件中的所有行，返回一个字符串列表
            for line in lines:
                stripped_line = line.strip()  # 去除每行字符串两端的空白字符（如换行符、空格等）
                if stripped_line:  # 如果处理后的行不为空字符串，则表示该行包含有效数据
                    row = stripped_line.split(',')  # 以逗号为分隔符将字符串拆分成列表，得到每行数据的各个字段
                    features = [float(x) for x in row[:-1]]  # 使用列表推导式将除最后一个字段（标签）之外的所有字段转换为浮点数，作为特征值
                    label = row[-1]  # 取每行数据的最后一个字段作为标签
                    data.append((features, label))  # 将特征列表和标签组成元组，添加到数据列表中
    except FileNotFoundError:
        print(f"Error: 文件 {file_path} 未找到，请检查文件路径。")
    except ValueError:
        print("Error: 文件中存在无法转换为浮点数的值，请检查数据格式。")
    return data


# 函数功能：计算两个点在多维空间中的欧氏距离，用于衡量数据点之间的相似性
# 参数说明：
# - point1 (list or tuple)：表示第一个点的坐标，是一个包含多个数值的可迭代对象，每个数值对应一个维度上的坐标值
# - point2 (list or tuple)：表示第二个点的坐标，与 point1 具有相同的结构和维度
# 返回值说明：
# - 返回一个浮点数，表示两个点之间的欧氏距离，即两点在多维空间中的直线距离
def euclidean_distance(point1, point2):
    # 使用生成器表达式计算两点对应坐标差值的平方和，相较于列表推导式，生成器表达式更节省内存，尤其在处理大规模数据时优势明显
    # zip 函数将两个点的坐标按维度一一对应打包成元组，方便计算差值
    diff_squared_sum = sum((x - y) ** 2 for x, y in zip(point1, point2))
    return math.sqrt(diff_squared_sum)  # 对差值的平方和开平方，得到欧氏距离


# 函数功能：基于KNN算法对给定的测试点进行分类，通过计算测试点与训练集中各点的距离，选取最近的k个邻居，根据邻居中多数的类别来预测测试点的类别
# 参数说明：
# - train_data (list of tuples)：训练数据集，每个元素是一个包含特征列表和标签的元组，用于构建分类模型
# - test_point (list or tuple)：待分类的测试点的特征值，是一个包含多个数值的可迭代对象，与训练集中的特征具有相同的维度
# - k (int)：一个正整数，表示在分类过程中要考虑的最近邻居的数量，k的选择会影响分类结果的准确性和稳定性
# 返回值说明：
# - 返回一个字符串，表示预测的测试点的类别标签
def knn_classify(train_data, test_point, k):
    # 使用列表推导式计算测试点与训练集中每个点的欧氏距离，并将距离和对应的标签组成元组，得到一个包含所有距离-标签对的列表
    distances = [(euclidean_distance(features, test_point), label) for features, label in train_data]
    # 使用 sorted 函数对距离列表进行排序，排序的依据是每个元组中的距离值（即第一个元素），lambda 函数指定了排序的键
    # 排序后取前k个元素，得到距离测试点最近的k个邻居的距离-标签对列表
    nearest_neighbors = sorted(distances, key=lambda x: x[0])[:k]
    # 使用列表推导式从最近邻居列表中提取出标签，得到一个仅包含标签的列表
    labels = [label for _, label in nearest_neighbors]
    # 使用 Counter 类统计标签列表中每个标签出现的次数，返回一个字典，其中键是标签，值是出现次数
    # most_common(1) 方法返回出现次数最多的一个标签及其次数，取第一个元素（即标签）作为预测的类别标签
    most_common_label = Counter(labels).most_common(1)[0][0]
    return most_common_label


# 函数功能：将数据集随机划分为训练集和测试集，用于模型的训练和评估
# 参数说明：
# - data (list)：原始数据集，是一个包含特征-标签对的列表，每个元素为一个元组，元组的第一个元素是特征列表，第二个元素是对应的标签字符串
# - test_size (float, 可选)：一个介于0和1之间的浮点数，表示测试集在整个数据集中所占的比例，默认值为0.2，即20%
# 返回值说明：
# - 返回一个包含两个元素的元组，第一个元素是训练集，第二个元素是测试集，两者均为特征-标签对的列表，且训练集和测试集的划分是随机的
def train_test_split(data, test_size=0.2):
    random.shuffle(data)  # 随机打乱数据集，确保数据的随机性，避免数据顺序对划分结果产生影响
    split_index = int(len(data) * (1 - test_size))  # 根据测试集比例计算划分索引，确定训练集和测试集的分割点
    return data[:split_index], data[split_index:]  # 返回划分后的训练集和测试集


if __name__ == "__main__":
    # 加载Iris数据集
    data = load_iris_data()
    if not data:
        print("数据加载失败，程序终止。")
    else:
        # 将数据集划分为训练集和测试集
        train_data, test_data = train_test_split(data)
        k = 3  # 定义KNN算法中的k值为3，即选择最近的3个邻居进行分类决策
        # 使用生成器表达式计算测试集中每个样本的预测结果是否与真实标签一致，返回一个布尔值列表
        # knn_classify 函数用于对每个测试样本进行分类预测，然后与真实标签进行比较
        correct_predictions = sum(knn_classify(train_data, features, k) == label for features, label in test_data)
        accuracy = correct_predictions / len(test_data)  # 计算预测正确的样本数占测试集样本总数的比例，得到分类准确率
        print(f"KNN分类准确率: {accuracy * 100:.2f}%")